﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Tao.OpenGl;
using System.Windows.Forms;
using CutAways.Geometry;
using CutAways.Illustration;
using CutAways.Shaders;

namespace CutAways.Modeling
{
	[Serializable]
	class Model
	{
		private string name = "";

		private Vertex[] vertices = null;
		private Vertex[] normals = null;
		private Vertex2D[] textureCoordinates = null;
		private Material[] materials = null;

		private GeometricObject[] objects = null;
		private GeometricObject selectedObject = null;

		private bool drawBoundingBoxes = false;

		private bool drawMaterials = true;

		// -------------------
		private ViewpointSet viewpointSet = new ViewpointSet();
		// -------------------

		public bool DrawBoundingBoxes
		{
			get { return drawBoundingBoxes; }
			set { drawBoundingBoxes = value; }
		}

		public GeometricObject SelectedObject
		{
			get { return selectedObject; }
			set
			{
				GeometricObject oldValue = selectedObject;
				selectedObject = value;
				if (SelectedObjectChanged != null && oldValue != selectedObject)
					SelectedObjectChanged(this, EventArgs.Empty);
			}
		}

		public ViewpointSet ViewpointSet
		{
			get { return viewpointSet; }
			set { viewpointSet = value; }
		}

		public bool DrawMaterials
		{
			get { return drawMaterials; }
			set { drawMaterials = value; }
		}

		public event EventHandler SelectedObjectChanged;

		public Vertex[] Vertices
		{
			get { return vertices; }
			set { vertices = value; }
		}
		public Vertex[] Normals
		{
			get { return normals; }
			set { normals = value; }
		}
		public Vertex2D[] TextureCoordinates
		{
			get { return textureCoordinates; }
			set { textureCoordinates = value; }
		}
		public Material[] Materials
		{
			get { return materials; }
			set { materials = value; }
		}
		public string Name
		{
			get { return name; }
			set { name = value; }
		}
		public GeometricObject[] Objects
		{
			get { return objects; }
			set { objects = value; }
		}

		/// <summary>
		/// vykresli vsechny podobjekty modelu, ktere se maji vykreslit (maji nastaven priznak DrawObject)
		/// </summary>
		public void Draw()
		{
			if (objects == null)
				return;
			Gl.glInitNames();
			Gl.glPushName(0);
			foreach (GeometricObject o in objects)
			{
				if (o.Visible)
				{
					if (o == selectedObject)
					{
						Gl.glColor3f(1f, 0.2f, 0f);
						o.DrawBoundingBox(); // draw bounding box of selected object
						drawObjectSelected(o); // draw selected object
					}
					else
					{
						if (this.drawBoundingBoxes) // draw its bounding box
						{
							Gl.glColor3f(0f, 0.6f, 1.0f);
							o.DrawBoundingBox();
						}

						//?????????????????? draw cut
						//Gl.glColor3f(1f, 0f, 0f);
						//o.DrawCut();
						//??????????????????

						DrawObject(o); // draw object
					}
					
				}
			}
		}

        public void DrawWithCuts(GeometricObject focusedObject, OcclusionGraph occlusionGraph, float openFactor, BoxCutShader boxCutShader, TubeCutShader tubeCutShader)
        {
            if (objects == null)
                return;
            
           	var objectsToCut = GetObjectsToCut(focusedObject, occlusionGraph);
			var remainingObjects = this.objects.Except(objectsToCut);

            float[] ambient = { 0.2f, 0.2f, 0.2f, 1 };
            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_AMBIENT, ambient);
            float[] diffuse = { 0.4f, 0.4f, 0.4f, 1 };
            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_DIFFUSE, diffuse);
            float[] specular = { 0.4f, 0.4f, 0.4f, 1 };
            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_SPECULAR, specular);
            float[] globalAmbient = { 0, 0, 0, 1 };
            Gl.glLightModelfv(Gl.GL_LIGHT_MODEL_AMBIENT, globalAmbient);

            float[] ambient2 = { 0.4f, 0.4f, 0.4f, 1 };
            float[] diffuse2 = { 1, 1, 1, 1 };
            float[] specular2 = { 1, 1, 1, 1 };
            float[] globalAmbient2 = { 0, 0, 0, 1 };

			// vykreslim oriznute objekty
			foreach (GeometricObject o in objectsToCut.Where(x => x.Visible))
			{
				if (o.CuttingParametres == null) // ??nastane to??
				{
					Gl.glUseProgram(0);
					//Gl.glColor3f(1f, 0.2f, 0f);
					//o.DrawBoundingBox(); // draw bounding box of selected object
					//drawObjectSelected(o); // draw selected object
				}
				else // set cut shader
				{
					if (o.CuttingType == CuttingType.Box && boxCutShader != null)
					{
						Vertex[] vertexArray = { o.OrientedBoundingBox.Points[0], o.OrientedBoundingBox.Points[1], o.OrientedBoundingBox.Points[4], o.OrientedBoundingBox.Points[2] };
						Interval[] finalCuttingParametres = getFinalCuttingParametres(o, focusedObject, occlusionGraph, openFactor);
						bool status = boxCutShader.SetParameters(vertexArray, finalCuttingParametres);
					}
					else if (o.CuttingType == CuttingType.WedgeTube && tubeCutShader != null)
					{
                        Vector axis = o.BoundingTube.FrontCenter - o.BoundingTube.BackCenter;
                        Vector reference = Vector.Norm(Vector.Cross(axis, Vector.XAxis)); // bod od ktereho se pocitaji uhly rezu
                        Interval[] finalCuttingParametres = getFinalCuttingParametres(o, focusedObject, occlusionGraph, openFactor);
                        bool status = tubeCutShader.SetParameters(o.BoundingTube.BackCenter, o.BoundingTube.FrontCenter, reference, finalCuttingParametres); // tube shade setup
					}
				}

				DrawObject(o); // draw object

                //Gl.glUseProgram(0);
                //Gl.glColor3f(0f, 0.6f, 1.0f);
                //o.DrawCut();

				if (this.drawBoundingBoxes) // draw its bounding box
				{
					Gl.glUseProgram(0);
					Gl.glColor3f(0f, 0.6f, 1.0f);
					o.DrawBoundingBox();
				}
			}
            Gl.glUseProgram(0);

            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_AMBIENT, ambient);
            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_DIFFUSE, diffuse);
            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_SPECULAR, specular);
            Gl.glLightModelfv(Gl.GL_LIGHT_MODEL_AMBIENT, globalAmbient);

			
			// vykreslim zbyle objekty, co jsou videt
            foreach (GeometricObject o in remainingObjects.Where(x => x.Visible))
            {
                if (o == focusedObject)
                {
                    Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_AMBIENT, ambient2);
                    Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_DIFFUSE, diffuse2);
                    Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_SPECULAR, specular2);
                    Gl.glLightModelfv(Gl.GL_LIGHT_MODEL_AMBIENT, globalAmbient2);
                }

                DrawObject(o);
				
                if (o == focusedObject)
                {
                    Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_AMBIENT, ambient);
                    Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_DIFFUSE, diffuse);
                    Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_SPECULAR, specular);
                    Gl.glLightModelfv(Gl.GL_LIGHT_MODEL_AMBIENT, globalAmbient);
                }

				if (this.drawBoundingBoxes) // draw its bounding box
				{
					Gl.glColor3f(0f, 0.6f, 1.0f);
					o.DrawBoundingBox();
				}
            }

        }

		private Interval[] getFinalCuttingParametres(GeometricObject objectToCut, GeometricObject focusedObject, OcclusionGraph occlusionGraph, float openFactor)
		{
			float expandFactor = 1.0f; // zavisly na hloubce - cim blize k pozorovateli, tim vetsi => postupne rozevirani rezu
			int distance = occlusionGraph.GetDistanceBetween(focusedObject, objectToCut) ?? 0;

			//MessageBox.Show("focused: " + occlusionGraph.GetDepthOf(focusedObject) + " to cut: " + occlusionGraph.GetDepthOf(objectToCut), "Depths");

			expandFactor = 1f + distance * MyGLControl.LEVEL_INCREMENT_OF_CUT;
			Interval[] result = new Interval[objectToCut.CuttingParametres.Length];

            for (int i = 0; i < result.Length; i++)
               result[i] = objectToCut.CuttingParametres[i] * (expandFactor * openFactor); // zmena sirky intervalu

			return result;
		}

		public List<GeometricObject> GetObjectsToCut(GeometricObject focusedObject, OcclusionGraph occlusionGraph)
		{
			List<GeometricObject> result = new List<GeometricObject>();
			if (focusedObject == null)
				return result;

			Queue<GeometricObject> queue = new Queue<GeometricObject>(occlusionGraph.GetParentsOf(focusedObject));
			while (queue.Count > 0)
			{
				GeometricObject peek = queue.Dequeue();
				result.Add(peek);
				foreach (GeometricObject o in occlusionGraph.GetParentsOf(peek))
					if (!queue.Contains(o))
						queue.Enqueue(o);
			}

			return result;
		}

		private void drawObjectSelected(GeometricObject o)
		{
			RenderMode temp = o.RenderMode;
			o.RenderMode |= RenderMode.Wired;
			DrawObject(o);
			o.RenderMode = temp;
		}

		public void DrawObject(GeometricObject o)
		{
			int isTexture = Gl.glIsEnabled(Gl.GL_TEXTURE_2D);
			
			if ((o.RenderMode & RenderMode.Texture) == RenderMode.Texture)
				Gl.glEnable(Gl.GL_TEXTURE_2D);

			if((o.RenderMode & RenderMode.Wired) == RenderMode.Wired)
				Gl.glPolygonMode(Gl.GL_FRONT_AND_BACK, Gl.GL_LINE);

			Gl.glLoadName(o.Index);

			
			for (int i = 0; i < o.Triangles.Length; i++)
			{
				Triangle t = o.Triangles[i];
				
				// ==============================================================================
				// tahle cast kodu to u me zle zpomaluje, kdyz se to zakomentuje, tak to chodi rychle, ale zas je to cely sedivy, bez barev
				// >>
				if (this.drawMaterials && (t.MaterialIndex >= 0) && (t.MaterialIndex < materials.Length))
				{
					if ((o.RenderMode & RenderMode.Material) == RenderMode.Material)
					{
						Gl.glMaterialfv(Gl.GL_FRONT_AND_BACK, Gl.GL_AMBIENT, materials[t.MaterialIndex].Ambient);
						Gl.glMaterialfv(Gl.GL_FRONT_AND_BACK, Gl.GL_DIFFUSE, materials[t.MaterialIndex].Diffuse);
						Gl.glMaterialfv(Gl.GL_FRONT_AND_BACK, Gl.GL_SPECULAR, materials[t.MaterialIndex].Specular);
						Gl.glMaterialf(Gl.GL_FRONT_AND_BACK, Gl.GL_SHININESS, materials[t.MaterialIndex].Shininess);
					}
					if (((o.RenderMode & RenderMode.Texture) == RenderMode.Texture) && (materials[t.MaterialIndex].TextureIndex >= 0))
						Gl.glBindTexture(Gl.GL_TEXTURE_2D, materials[t.MaterialIndex].TextureIndex);
				}
				// <<
				// ==============================================================================

                Gl.glBegin(Gl.GL_TRIANGLES);
				
                if ((t.Normal != Vertex.Empty) && ((o.RenderMode & RenderMode.Flat) == RenderMode.Flat))
					Gl.glNormal3f(t.Normal.X, t.Normal.Y, t.Normal.Z);

				for (int index = 0; index < 3; index++)
				{
					if ((t.NormalIndices[index] >= 0) && (t.NormalIndices[index] < normals.Length) && ((o.RenderMode & RenderMode.Smooth) == RenderMode.Smooth))
						Gl.glNormal3f(normals[t.NormalIndices[index]].X, normals[t.NormalIndices[index]].Y, normals[t.NormalIndices[index]].Z);

					if ((t.TextureCoordinateIndices[index] >= 0) && (t.TextureCoordinateIndices[index] < textureCoordinates.Length) && ((o.RenderMode & RenderMode.Texture) == RenderMode.Texture))
						Gl.glTexCoord2f(textureCoordinates[t.TextureCoordinateIndices[index]].X, textureCoordinates[t.TextureCoordinateIndices[index]].Y);

					Gl.glVertex3f(vertices[t.VertexIndices[index]].X, vertices[t.VertexIndices[index]].Y, vertices[t.VertexIndices[index]].Z);
				}
                Gl.glEnd();
			}
			

			if (isTexture == 1)
				Gl.glEnable(Gl.GL_TEXTURE_2D);
			else
				Gl.glDisable(Gl.GL_TEXTURE_2D);
			if ((o.RenderMode & RenderMode.Wired) == RenderMode.Wired)
				Gl.glPolygonMode(Gl.GL_FRONT_AND_BACK, Gl.GL_FILL);
		}

		public void SelectObjectOnPosition(Camera viewpoint, int x, int y)
		{
			SelectedObject = GetObjectOnPosition(viewpoint, x, y);
		}

		public GeometricObject GetObjectOnPosition(Camera viewpoint, int x, int y)
		{
			int hits = 0;
			int[] viewport = new int[4];
			int[] buffer = new int[64];

			GeometricObject result = null;

			Gl.glSelectBuffer(buffer.Length, buffer);
			Gl.glGetIntegerv(Gl.GL_VIEWPORT, viewport);


			// Switch to projection and save the matrix
			Gl.glMatrixMode(Gl.GL_PROJECTION);
			Gl.glPushMatrix();
			// Change render mode
			Gl.glRenderMode(Gl.GL_SELECT);
			// Establish new clipping volume to be unit cube around
			// mouse cursor point (xPos, yPos) and extending two pixels
			// in the vertical and horizontal direction
			Gl.glLoadIdentity();
			Glu.gluPickMatrix(x, viewport[3] - y + viewport[1], 2, 2, viewport);
			
			// Apply perspective matrix
			float aspect = (float)viewport[2] / (float)viewport[3];
			//Glu.gluPerspective(45.0f, aspect, 1.0, 425.0);
			Glu.gluPerspective(90.0f, aspect, 0.1f, 100.0f);
			

			// Draw the scene
			Draw();
			// Collect the hits
			hits = Gl.glRenderMode(Gl.GL_RENDER);
			// If a single hit occurred, display the info.
			if (hits > 0)
			{
				int depth = buffer[1];					// Store How Far Away It Is
				int choose = buffer[3];					// Make Our Selection The First Object
				
				for (int loop = 0; loop < hits; loop++)				// Loop Through All The Detected Hits
				{
					//int namesCount = buffer[loop * 4];
					// If This Object Is Closer To Us Than The One We Have Selected
					if (buffer[loop * 4 + 1] < depth)
					{
						choose = buffer[loop * 4 + 3];			// Select The Closer Object
						depth = buffer[loop * 4 + 1];			// Store How Far Away It Is
					}
					//MessageBox.Show(objects[choose].Name + "; " + loop + "; " + i + Environment.NewLine + "depth: " + depth + "; " + buffer[loop * 4 + 2]);
				}
				
				result = objects[choose];
			}


			// Restore the projection matrix
			Gl.glMatrixMode(Gl.GL_PROJECTION);
			Gl.glPopMatrix();
			// Go back to modelview for normal rendering
			Gl.glMatrixMode(Gl.GL_MODELVIEW);

			return result;
		}

		public float Normalize()
		{
			float scale = 0;

			if (vertices.Length <= 0) { return 0; }

			float maxx, minx, maxy, miny, maxz, minz;
			float cx, cy, cz, w, h, d;

			maxx = minx = vertices[0].X;
			maxy = miny = vertices[0].Y;
			maxz = minz = vertices[0].Z;

			for (int i = 1; i < vertices.Length; i++)
			{
				if (maxx < vertices[i].X) { maxx = vertices[i].X; }
				if (minx > vertices[i].X) { minx = vertices[i].X; }
				if (maxy < vertices[i].Y) { maxy = vertices[i].Y; }
				if (miny > vertices[i].Y) { miny = vertices[i].Y; }
				if (maxz < vertices[i].Z) { maxz = vertices[i].Z; }
				if (minz > vertices[i].Z) { minz = vertices[i].Z; }
			}

			w = Math.Abs(maxx) + Math.Abs(minx);
			h = Math.Abs(maxy) + Math.Abs(miny);
			d = Math.Abs(maxz) + Math.Abs(minz);

			cx = (maxx + minx) / 2.0f;
			cy = (maxy + miny) / 2.0f;
			cz = (maxz + minz) / 2.0f;

			scale = 2.0f / Math.Max(Math.Max(w, h), d);

			for (int i = 0; i < vertices.Length; i++)
			{
				vertices[i].X -= cx;
				vertices[i].Y -= cy;
				vertices[i].Z -= cz;
				vertices[i].X *= scale;
				vertices[i].Y *= scale;
				vertices[i].Z *= scale;
			}

			foreach (GeometricObject o in this.objects) // spocitat stredy
				o.CreateBoundingBox(this);

			return scale;
		}
	}
}
